﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Web.Services.Protocols;
using System.Xml;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Win32;

using ModifyCrmCA.Resources;

using MetadataSdk;
using CrmSdk;
using CrmSdk.Discovery;

namespace ModifyCrmCA
{
    public class CustomActions
    {
        private const int AD = 0;
        private const int Passport = 1;
        private const int Spla = 2;

        private const int StringPick = 1;
        private const int BooleanPick = 3;

        private static Session _session;

        static CrmService _crmService;
        static MetadataService _metadataService;
        static CrmDiscoveryService _discoveryService;
        static EventWaitHandle _waitForAsync = new EventWaitHandle(true, EventResetMode.ManualReset);
        static volatile int _activeAsyncCalls = 0;
        static volatile bool _asyncSuccess;

        const string ButtonEntityName = "new_agentcommunicationsbutton";
        const string ParameterEntityName = "new_agentcommunicationsparameter";

        const string ButtonPrivilegeName = "prvReadNew_agentcommunicationsbutton";
        const string ParameterPrivilegeName = "prvReadNew_agentcommunicationsparameter";


        [CustomAction]
        public static ActionResult Install(Session session)
        {
            _session = session;
            Log("Begin Install");

            try
            {
                InitializeServices(session);
                return Install();
            }
            catch (SoapException e)
            {
                Log(String.Format("SoapException : {0} : {1}", e.Message, e.StackTrace));
            }
            catch (WebException e)
            {
                Log(String.Format("WebException : {0} : {1}", e.Message, e.StackTrace));
            }
            catch (Exception e)
            {
                Log(String.Format("Exception : {0} : {1}", e.Message, e.StackTrace));
            }
            return ActionResult.Failure;
        }

        [CustomAction]
        public static ActionResult Uninstall(Session session)
        {
            _session = session;
            Log("Begin Uninstall");

            try
            {
                InitializeServices(session);
                Uninstall();
                return ActionResult.Success; 
            }
            catch (SoapException e)
            {
                Log(String.Format("SoapException : {0} : {1}", e.Message, e.StackTrace));
            }
            catch (WebException e)
            {
                Log(String.Format("WebException : {0} : {1}", e.Message, e.StackTrace));
            }
            catch (Exception e)
            {
                Log(String.Format("Exception : {0} : {1}", e.Message, e.StackTrace));
            }
            return ActionResult.Failure;
        }


        /// <summary>
        /// Install Steps
        /// </summary>
        private static ActionResult Install()
        {
            ImportCustomEntities();
            UpdateSiteMap();

            if (AddDefaultParameters() &&
                SetCustomEntityPermissions())
            {
                return ActionResult.Success;
            }
            else
            {
                return ActionResult.Failure;
            }
        }

        /// <summary>
        /// Add the individual default empty parameters.
        /// </summary>
        private static bool AddDefaultParameters()
        {
            Log("Begin AddDefaultParameters");
            

            InitializeAsyncCalls(3);

            AddDefaultParameter("ResponseGroupServiceIntegration", "False", BooleanPick);
            AddDefaultParameter("ResponseGroupServiceWebServiceUri", "", StringPick);
            AddDefaultParameter("CustomerFormDisplayed", "Contact", StringPick);

            _waitForAsync.WaitOne();
            return _asyncSuccess;
        }

        /// <summary>
        /// Add a parameter
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <param name="picklistValue"></param>
        private static void AddDefaultParameter(string name, string value, int picklistValue)
        {
            Log(String.Format("AddDefaultParameter name:{0}, value:{1}", name, value));

            StringProperty parameterName = new StringProperty();
            parameterName.Name = "new_name";
            parameterName.Value = name;

            StringProperty parameterValue = new StringProperty();
            parameterValue.Name = "new_parametervalue";
            parameterValue.Value = value;

            PicklistProperty parameterPicklist = new PicklistProperty();
            parameterPicklist.Name = "new_parametertype";
            parameterPicklist.Value = new Picklist();
            parameterPicklist.Value.Value = picklistValue;

            DynamicEntity parameterEntity = new DynamicEntity();
            parameterEntity.Name = ParameterEntityName;
            parameterEntity.Properties = new Property[] { parameterName, parameterValue, parameterPicklist };

            TargetCreateDynamic targetCreate = new TargetCreateDynamic();
            targetCreate.Entity = parameterEntity;

            CreateRequest createRequest = new CreateRequest();
            createRequest.Target = targetCreate;

            _crmService.BeginExecute(createRequest, ParameterAdded, name);

        }

        static void ParameterAdded(IAsyncResult asyncResult)
        {
            string name = asyncResult.AsyncState as string;
            Log(String.Format("ParameterAdded name:{0}", name));

            try
            {
                _crmService.EndExecute(asyncResult);
            }
            catch (Exception e)
            {
                _session.Log("ParameterAdded - Exception : {0} : {1}", e.Message, e.StackTrace);
                _asyncSuccess = false;
            }

            DecrementAsyncCalls();
        }

        static void InitializeServices(Session session)
        {
            Log("Begin InitializeServices");

            string username = session["SPLAUSERNAME"];
            string password = session["SPLAPASSWORD"];
            int installType;

            if (username != "" && password != "")
            {
                Log("SPLA authentication detected");
                installType = Spla;

            }
            else
            {
                Log("AD authentication detected");
                installType = AD;
            }

            InitializeServices(installType, username, password);

        }

        /// <summary>
        /// Initialize the CRM and MetaData services.
        /// </summary>
        /// <param name="authType"></param>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        static void InitializeServices(int authType, string username, string password)
        {
            _crmService = new CrmService();
            _metadataService = new MetadataService();
            _discoveryService = new CrmDiscoveryService();

            RegistryKey CrmRegKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSCRM");
            string WebServiceUrl = CrmRegKey.GetValue("ServerURL") as string;

            // Get the organization deails so we can initialize the CrmService and MetadataService
            OrganizationDetail orgDetail = GetOrganizationDetails(authType, WebServiceUrl, username, password);

            _crmService.Url = orgDetail.CrmServiceUrl;
            _metadataService.Url = orgDetail.CrmMetadataServiceUrl;

            Log(String.Format("_crmService.Url = {0}", _crmService.Url));
            Log(String.Format("_metadataService.Url = {0}", _metadataService.Url));

            CrmSdk.CrmAuthenticationToken crmAuthToken = new CrmSdk.CrmAuthenticationToken();
            crmAuthToken.AuthenticationType = authType;
            crmAuthToken.OrganizationName = orgDetail.OrganizationName;

            MetadataSdk.CrmAuthenticationToken metadataAuthToken = new MetadataSdk.CrmAuthenticationToken();
            metadataAuthToken.AuthenticationType = authType;
            metadataAuthToken.OrganizationName = orgDetail.OrganizationName;


            if (AD == authType)
            {
                _crmService.UseDefaultCredentials = true;
                _crmService.Credentials = CredentialCache.DefaultCredentials;

                _metadataService.UseDefaultCredentials = true;
                _metadataService.Credentials = CredentialCache.DefaultCredentials;
            }
            else
            {
                _crmService.UseDefaultCredentials = false;
                RetrieveCrmTicketRequest ticketRequest = new RetrieveCrmTicketRequest();
                ticketRequest.OrganizationName = orgDetail.OrganizationName;
                ticketRequest.UserId = username;
                ticketRequest.Password = password;

                RetrieveCrmTicketResponse ticketResponse;

                ticketResponse = (RetrieveCrmTicketResponse)_discoveryService.Execute(ticketRequest);

                crmAuthToken.CrmTicket = ticketResponse.CrmTicket;
                metadataAuthToken.CrmTicket = ticketResponse.CrmTicket;
            }

            _crmService.CrmAuthenticationTokenValue = crmAuthToken;
            _metadataService.CrmAuthenticationTokenValue = metadataAuthToken;

            return;
        }

        /// <summary>
        /// Retrieve the organization details.  We assume that there is only 1 organization on the CRM install.
        /// </summary>
        /// <param name="authType"></param>
        /// <param name="webServiceUrl"></param>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        static OrganizationDetail GetOrganizationDetails(int authType, string webServiceUrl, string username, string password)
        {
            Log("Begin GetOrganizationDetails");

            RetrieveOrganizationsRequest orgRequest = new RetrieveOrganizationsRequest();

            string authExtension;

            if (authType == AD)
            {
                authExtension = "AD";

                _discoveryService.UseDefaultCredentials = true;
                _discoveryService.Credentials = CredentialCache.DefaultCredentials;
            }
            else
            {
                authExtension = "SPLA";
                _discoveryService.UseDefaultCredentials = false;
                orgRequest.Password = password;
                orgRequest.UserId = username;
            }


            _discoveryService.Url = String.Format("{0}/2007/{1}/CrmDiscoveryService.asmx", webServiceUrl, authExtension);
            RetrieveOrganizationsResponse orgResponse = (RetrieveOrganizationsResponse)_discoveryService.Execute(orgRequest);

            return orgResponse.OrganizationDetails[0];
        }

        /// <summary>
        /// Update the site map with the new custom entities.
        /// </summary>
        static void UpdateSiteMap()
        {
            Log("Begin UpdateSiteMap");

            // Create the request.
            ExportXmlRequest exportRequest = new ExportXmlRequest();

            // Define the entities to retrieve
            exportRequest.ParameterXml = @"<importexportxml>
<entities>
</entities>
<nodes>  
<node>sitemap</node>
</nodes>
<securityroles>                       
</securityroles>
<settings>
</settings>
<workflows></workflows>
</importexportxml>";

            ExportXmlResponse exportResponse = (ExportXmlResponse)_crmService.Execute(exportRequest);

            // Create the XML DOM.
            XmlDocument siteMapDocument = new XmlDocument();
            siteMapDocument.PreserveWhitespace = true;
            siteMapDocument.LoadXml(exportResponse.ExportXml);

            // Cretae the 2 new "SubArea" nodes.
            XmlElement buttonElement = CreateNewSubArea(siteMapDocument, ButtonEntityName);
            XmlElement parametersElement = CreateNewSubArea(siteMapDocument, ParameterEntityName);

            // Look for an existing "Extensions" node under the "Settings" node.
            XmlNode settingsNode = siteMapDocument.SelectSingleNode("//Area[@Id='Settings']");
            XmlNode extensionsNode = settingsNode.SelectSingleNode("//Area[@Id='Settings']/Group[@Id='Extensions']");

            if (extensionsNode == null)
            {
                // No "Extensions" node was located so create a new one and append the new XmlElements.
                XmlElement newExtensionsNode = siteMapDocument.CreateElement("Group");
                newExtensionsNode.SetAttribute("Id", "Extensions");
                newExtensionsNode.SetAttribute("ResourceId", "Group_Extensions");
                newExtensionsNode.AppendChild(buttonElement);

                TryAppendChild(newExtensionsNode, buttonElement, ButtonEntityName);
                TryAppendChild(newExtensionsNode, parametersElement, ParameterEntityName);

                settingsNode.AppendChild(newExtensionsNode);
            }
            else
            {
                // An existing "Extensions" node was located append the new XmlElements to the existing
                // "Extensions" node.
                XmlNode newExtensionsNode = extensionsNode.Clone();
                TryAppendChild(newExtensionsNode, buttonElement, ButtonEntityName);
                TryAppendChild(newExtensionsNode, parametersElement, ParameterEntityName);
                settingsNode.ReplaceChild(newExtensionsNode, extensionsNode);
            }

            ImportAndPublishXml(siteMapDocument.OuterXml);
        }

        /// <summary>
        /// Create a new XmlElement "SubArea" Node.
        /// </summary>
        /// <param name="siteMapDocument"></param>
        /// <param name="customEntity"></param>
        /// <returns></returns>
        private static XmlElement CreateNewSubArea(XmlDocument siteMapDocument, string customEntity)
        {
            XmlElement buttonElement = siteMapDocument.CreateElement("SubArea");
            buttonElement.SetAttribute("Id", customEntity);
            buttonElement.SetAttribute("Entity", customEntity);
            return buttonElement;
        }

        /// <summary>
        /// Import and publish the XML string.
        /// </summary>
        /// <param name="xml"></param>
        static void ImportAndPublishXml(string xml)
        {
            ImportAllXmlRequest importAllRequest = new ImportAllXmlRequest();
            importAllRequest.CustomizationXml = xml;
            _crmService.Execute(importAllRequest);

            PublishAllXmlRequest publishRequest = new PublishAllXmlRequest();
            _crmService.Execute(publishRequest);
        }

        /// <summary>
        /// Append a child XmlElement if the parent does not already contain a child element with matching
        /// "Id" property.
        /// </summary>
        /// <param name="ParentElement"></param>
        /// <param name="ChildElement"></param>
        /// <param name="Id"></param>
        /// <returns></returns>
        static bool TryAppendChild(XmlNode ParentElement, XmlElement ChildElement, string Id)
        {
            XmlNode testForIdNode = ParentElement.SelectSingleNode(String.Format("SubArea[@Id='{0}']", Id));
            if (null == testForIdNode)
            {
                ParentElement.AppendChild(ChildElement);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Import the custom entities.
        /// </summary>
        static void ImportCustomEntities()
        {
            Log("Begin ImportCustomEntities");

            /// Import and publish the XML string stored as a resource.
            ImportAndPublishXml(Resource.customizations);
        }

        /// <summary>
        /// Ensuree that all security roles have permissions to read the new custom entities
        /// </summary>
        static bool SetCustomEntityPermissions()
        {
            Log("Begin SetCustomEntityPermissions");

            RolePrivilege[] adbPrivileges = GetAdbPrivileges();

            QueryExpression query = new QueryExpression();
            query.ColumnSet = new AllColumns();
            query.EntityName = "role";

            RetrieveMultipleRequest request = new RetrieveMultipleRequest();
            request.Query = query;
            request.ReturnDynamicEntities = true;

            RetrieveMultipleResponse response = _crmService.Execute(request) as RetrieveMultipleResponse;
            BusinessEntityCollection businessEntityCollection = response.BusinessEntityCollection;

            InitializeAsyncCalls(businessEntityCollection.BusinessEntities.Length);


            //$$TODO(160062) - Don't try to update permission for admin and power users.
            foreach (DynamicEntity entity in businessEntityCollection.BusinessEntities)
            {
                Guid roleId = entity.Properties.GetProperty<KeyProperty>("roleid").Value.Value;

                AddPrivilegesRoleRequest addPrivilegesRequest = new AddPrivilegesRoleRequest();
                addPrivilegesRequest.Privileges = adbPrivileges;
                addPrivilegesRequest.RoleId = roleId;
                _crmService.BeginExecute(addPrivilegesRequest, PrivilegeAdded, entity);
            }

            _waitForAsync.WaitOne();
            return _asyncSuccess;
        }

        /// <summary>
        /// _asyncSuccess is not set to false if we fail to set a privilege.  There are a number of account which
        /// the privilege level is not adjustable.
        /// </summary>
        /// <param name="asyncResult"></param>
        static void PrivilegeAdded(IAsyncResult asyncResult)
        {
            try
            {
                _crmService.EndExecute(asyncResult);
            }
            catch (SoapException e)
            {
                Log(String.Format("PrivilegeAdded - SoapException - NonFatal : {0} : {1}", e.Message, e.StackTrace));
            }
            catch (Exception e)
            {
                Log(String.Format("PrivilegeAdded - Exception : {0} : {1}", e.Message, e.StackTrace));
                _asyncSuccess = false;
            }

            DecrementAsyncCalls();
        }

        static RolePrivilege[] GetAdbPrivileges()
        {
            RetrievePrivilegeSetRequest privilegeSetRequest = new RetrievePrivilegeSetRequest();
            RetrievePrivilegeSetResponse privilegeSetResponse = _crmService.Execute(privilegeSetRequest) as RetrievePrivilegeSetResponse;

            RolePrivilege[] adbPrivileges = new RolePrivilege[2];

            foreach (BusinessEntity entity in privilegeSetResponse.BusinessEntityCollection.BusinessEntities)
            {
                CrmSdk.privilege privilege = entity as CrmSdk.privilege;

                switch (privilege.name)
                {
                    case ButtonPrivilegeName:
                        adbPrivileges[0] = new RolePrivilege();
                        adbPrivileges[0].PrivilegeId = privilege.privilegeid.Value;
                        adbPrivileges[0].Depth = PrivilegeDepth.Global;
                        break;

                    case ParameterPrivilegeName:
                        adbPrivileges[1] = new RolePrivilege();
                        adbPrivileges[1].PrivilegeId = privilege.privilegeid.Value;
                        adbPrivileges[1].Depth = PrivilegeDepth.Global;
                        break;
                }
            }

            return adbPrivileges;
        }

        private static void Uninstall()
        {
            DeleteCustomEntity(ButtonEntityName);
            DeleteCustomEntity(ParameterEntityName);
        }

        /// <summary>
        /// Delete the custom entity.  Uses synchronous calls because multiple async calls
        /// fail.
        /// </summary>
        /// <param name="name"></param>
        private static void DeleteCustomEntity(string name)
        {
            Log(String.Format("Begin DeleteCustomEntity - name:{0}", name));

            DeleteEntityRequest deleteRequest = new DeleteEntityRequest();
            deleteRequest.LogicalName = name;
            DeleteEntityResponse deleteResponse = (DeleteEntityResponse)_metadataService.Execute(deleteRequest);
        }

        #region Async helpers

        private static void InitializeAsyncCalls(int count)
        {
            _asyncSuccess = true;
            _activeAsyncCalls = count;
            _waitForAsync.Reset();
        }

        private static void DecrementAsyncCalls()
        {
            _activeAsyncCalls--;

            if (_activeAsyncCalls == 0)
            {
                _waitForAsync.Set();
            }
        }

        #endregion

        private static void Log(string message)
        {
            _session.Log("ModifyCrmCA : {0}", message);
        }

    }

    public static class CrmServiceHelpers
    {
        public static T GetProperty<T>(this Property[] properties, string name)
        {
            foreach (Property property in properties)
            {
                if (property.Name.Equals(name))
                {
                    return property.AssertCast<T>();
                }
            }
            return default(T);
        }
    }

    /// <summary>
    /// Helper methods used in Office Communications Server applications.
    /// </summary>
    public static class DebugHelpers
    {
        /// <summary>
        /// Debug.Assert that value is of type T, then return (T)value 
        /// </summary>
        /// <typeparam name="T">Type of value</typeparam>
        /// <param name="value">Object to be cast to T</param>
        /// <returns>(T)value</returns>
        public static T AssertCast<T>(this object value)
        {
            Debug.Assert(null == value || value is T, "null == value || value is T");

            return (T)value;
        }
    }
}

